Map과 Object를 선택하는 실용적인 기준

Map과 Object를 선택하는 실용적인 기준

JavaScript 객체는 키-값 저장소처럼 쓸 수 있지만 본래 레코드와 동작을 표현하는 핵심 자료형이다. 동적으로 항목을 추가하고 삭제하는 컬렉션이라면 Map이 의도를 더 정확히 드러낼 수 있다.

한눈에 보기

  • 고정된 필드를 가진 데이터 모델은 Object가 자연스럽다.
  • 동적인 키, 잦은 추가·삭제, 키 타입 보존이 필요하면 Map을 고려한다.
  • 직렬화와 외부 API 경계에서는 Object가 편리하다.

목차

키의 타입과 충돌

객체의 일반 프로퍼티 키는 문자열이나 Symbol이다. 숫자 키도 문자열로 변환된다. Map은 객체를 포함한 값을 키로 그대로 사용할 수 있다.

const objectKey = { id: 1 };
const cache = new Map();

cache.set(objectKey, "cached");
console.log(cache.get(objectKey)); // cached

일반 객체를 사전으로 쓸 때는 Object.create(null)이나 hasOwn을 이용해 프로토타입 프로퍼티와 구분해야 한다.

순회와 크기 확인

Map은 size, keys, values, entries와 반복 프로토콜을 기본 제공한다.

const counts = new Map([
  ["apple", 2],
  ["banana", 3],
]);

for (const [name, count] of counts) {
  console.log(name, count);
}

직렬화 경계

JSON.stringify는 Map을 원하는 형태로 직렬화하지 않는다. API 응답이나 설정 파일처럼 JSON과 직접 오가는 데이터는 Object가 단순하다.

선택 질문

이것이 필드가 정해진 하나의 레코드인가, 아니면 런타임에 변하는 항목의 컬렉션인가를 먼저 묻는다.

문자열 키로 바뀌는 Object

일반 객체에서 숫자 키는 문자열 프로퍼티 키로 변환된다.

const object = {};
object[1] = "number";
object["1"] = "string";

console.log(object); // { "1": "string" }

두 입력은 같은 키를 가리킨다. 반면 Map은 키의 타입을 보존한다.

const map = new Map();
map.set(1, "number");
map.set("1", "string");

console.log(map.size); // 2

객체 자체를 키로 사용할 때는 참조 동일성이 기준이 된다.

const firstRequest = { id: 1 };
const sameLookingRequest = { id: 1 };
const cache = new Map();

cache.set(firstRequest, "cached result");

console.log(cache.get(firstRequest)); // cached result
console.log(cache.get(sameLookingRequest)); // undefined

객체 내용이 같다는 의미상의 동일성을 원한다면 Map이 자동으로 해결해 주지 않는다. id처럼 도메인 키를 선택해야 한다.

Object를 사전으로 쓸 때의 함정

사용자 입력을 일반 객체의 키로 바로 넣으면 prototype에 있는 이름과 충돌하거나 프로토타입 오염을 고려해야 한다.

const counts = {};

function increment(key) {
  counts[key] = (counts[key] ?? 0) + 1;
}

단순 사전이 필요하다면 Object.create(null)로 prototype이 없는 객체를 만들거나 Map을 사용할 수 있다.

const dictionary = Object.create(null);
dictionary.constructor = 1;

console.log(Object.hasOwn(dictionary, "constructor")); // true

일반 객체를 사용할 때 존재 여부는 in보다 Object.hasOwn이 의도에 맞는 경우가 많다.

const settings = { theme: "dark" };

console.log("toString" in settings); // true
console.log(Object.hasOwn(settings, "toString")); // false

캐시를 만들 때 Map이 편한 이유

Map은 삽입 순서를 순회할 수 있어 간단한 크기 제한 캐시를 설명하기 좋다.

function createLruLikeCache(maxSize) {
  const entries = new Map();

  return {
    get(key) {
      if (!entries.has(key)) return undefined;
      const value = entries.get(key);
      entries.delete(key);
      entries.set(key, value);
      return value;
    },

    set(key, value) {
      if (entries.has(key)) entries.delete(key);
      entries.set(key, value);

      if (entries.size > maxSize) {
        const oldestKey = entries.keys().next().value;
        entries.delete(oldestKey);
      }
    },
  };
}

이 코드는 LRU 개념을 보여주기 위한 예시다. 운영 캐시는 TTL, 동시성, 메모리 계산과 관측이 추가로 필요하다.

WeakMap은 언제 다른가

DOM 요소나 객체에 부가 데이터를 연결하되 그 객체의 가비지 컬렉션을 막고 싶지 않다면 WeakMap을 고려할 수 있다.

const metadata = new WeakMap();

function observe(element) {
  metadata.set(element, { observedAt: Date.now() });
}

WeakMap의 키는 객체여야 하고 전체 순회와 size 조회를 제공하지 않는다. 캐시 내용을 나중에 모두 출력해야 한다면 맞지 않는다.

질문 Object Map WeakMap
JSON으로 바로 직렬화할까 적합 변환 필요 불가
키가 동적으로 늘어날까 가능 적합 객체 키만 가능
객체를 키로 쓸까 문자열화됨 참조 키 가능 참조 키 가능
전체 순회가 필요할까 가능 가능 불가
키 객체의 수명을 붙잡지 않을까 해당 없음 붙잡음 붙잡지 않음

결론

Object와 Map의 차이는 성능 표 한 장보다 데이터의 의미에서 시작한다. 레코드는 Object, 동적 사전은 Map을 기본 후보로 두면 코드의 의도가 선명해진다.

관련 노트